package AddonPath;

use strict;
use warnings;

use File::Spec;
use File::Spec::Unix;    # Zip file names are in Unix format
use File::Path;
use File::Compare;
use File::Copy;
use IO::Dir;
use IO::Handle;
use Archive::Zip;
use Archive::Tar;
use Errno;
use File::Slurp;
use Hash::Util;
use Cwd qw(realpath);

use PerlIO::via::Bzip2;
use PerlIO::Gzip;

use Options;
use AddonInfo;
use AddonSVN;
use AddonGIT;
use AddonHG;
use Database;
use Constants qw( :dir :vinfo :source );
use AddonTemp;
use AddonDiff;
use TOCInfo;

BEGIN {
    use Exporter ();
    our ( $VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS );

    $VERSION     = sprintf( "%d", q$Revision$ =~ m{\d+} );
    @ISA         = qw(Exporter);
    @EXPORT      = qw(&get_downloads &split_entry);
    %EXPORT_TAGS = ();
    @EXPORT_OK   = qw();
} ## end BEGIN

# Fields
use constant field_base_name          => 0;
use constant field_enabled_base_path  => 1;
use constant field_disabled_base_path => 2;
use constant field_entry              => 3;
use constant field_nodes              => 4;
use constant field_group_okay         => 5;
use constant field_group_cache        => 6;
use constant field_is_install         => 7;
use constant field_is_disabled_dir    => 8;
use constant field_move_tried         => 9;

our %paths_by_enabled_path               = ();
our %ziparchives_by_file_key             = ();
our %zipfiles_by_file_key_by_file_ucname = ();
our %zipdirs_by_file_key_by_file_ucname  = ();
our %ziptimes_by_file_key                = ();

### Global functions
sub split_entry($ );    # name, revision, minor_rev, extension
sub get_downloads(;$ );

### Private functions
sub p_get_source_type($ );
sub p_get_version_info($ );
sub p_generate_base_name($$ );
sub p_compare_entries($$ );
sub p_move_path($$$ );
sub p_get_archive($ );
sub p_get_subaddons($ );

### Private methods
sub p_get_all_entries($ );
sub p_get_base_path($ );
sub p_get_zip($ ) { return p_get_archive( shift->get_real_path ); }
sub p_get_file_in_zip($$ );
sub p_get_dir_in_zip($$ );
sub p_node_ls($$ );
sub p_node_is_file($$ );
sub p_node_version_info($$ );
sub p_node_svn_externals($$ );
sub p_node_file_key_raw($$ );
sub p_node_file_key($$ );
sub p_node_contents($$ );
sub p_node_copy_to_path($$$ );
sub p_node_copy_to_node($$$$ );
sub p_node_diff_from_path($$$;$ );
sub p_node_diff_from_node($$$$;$ );
sub p_set_disabled($$ );

### Public
sub lookup($$;$ );    ## Also a method

### Methods
sub same_base_for($@ );
sub get_base_name($ ) { return shift->[ &field_base_name ]; }
sub get_base_key($ );
sub get_entry($ ) { return shift->[ &field_entry ]; }
sub change_entry($$ );
sub get_real_path($ );
sub is_stage_dir($ )    { return shift->[ &field_group_okay ]; }
sub is_install_dir($ )  { return shift->[ &field_is_install ]; }
sub is_disabled_dir($ ) { return shift->[ &field_is_disabled_dir ]; }
sub enable_dir($ )      { return shift->p_set_disabled_dir( 0 ); }
sub disable_dir($ )     { return shift->p_set_disabled_dir( 1 ); }
sub prune($ );
sub get_node_path($$ );
sub get_subaddon_nodes($ );

### Bodies

sub split_entry($ ) {
    my $entry     = shift;
    my $extension = "";
    if ( $entry =~ s{\.zip$}{}i ) {
        $extension = "zip";
    }
    elsif ( $entry =~ s{\.t(ar(?:\.(?:gz|bz2?|Z))?|gz|bz2?)$}{} ) {
        my $rest = $1;
        $extension = "tar";
        if ( $rest =~ m{gz$} ) {
            $extension .= ".gz";
        }
        elsif ( $rest =~ m{bz2$} ) {
            $extension .= ".bz2";
        }
        elsif ( $rest =~ m{Z$} ) {
            $extension .= ".Z";
        }
    } ## end elsif ( $entry =~ s{\.t(ar(?:\.(?:gz|bz2?|Z))?|gz|bz2?)$})
    my $rev       = undef;
    my $minor_rev = undef;
    if ( $entry =~ s{-r(\d+)(?:\.(\d+))?$}{} ) {
        $rev       = $1;
        $minor_rev = $2;
    }
    return $entry, $rev, $minor_rev, $extension;
} ## end sub split_entry($ )

our %downloads = ();

sub get_downloads(;$ ) {
    my $base_path = shift;
    $base_path ||= stage_dir;

    my $dload_dir = File::Spec->catdir( $base_path, &DIR_DOWNLOADS );
    my $res = $downloads{ $dload_dir };
    unless ( $res ) {
        $res = {};
        $downloads{ $dload_dir } = $res;
        my $dirh = new IO::Dir( $dload_dir );
        foreach my $entry ( File::Spec->no_upwards( $dirh->read ) ) {
            my $try_path    = File::Spec->catfile( $dload_dir, $entry );
            my $found       = undef;
            my $found_match = undef;
            foreach my $subaddons ( p_get_subaddons( $try_path ) ) {
                my $name = $subaddons->[ 0 ];
                $found = $name unless defined $found;
                if ( $entry =~ m{^$name$}i ) {
                    $found       = $name unless $found_match;
                    $found_match = 1;
                    $found       = $name if length( $name ) > length( $found );
                }
                elsif ( !$found_match ) {
                    $found = $name if length( $name ) < length( $found );
                }
            } ## end foreach my $subaddons ( p_get_subaddons...)
            $res->{ $found } = 1 if $found;
        } ## end foreach my $entry ( File::Spec...)
    } ## end unless ( $res )
    return sort keys %$res;
} ## end sub get_downloads(;$ )

sub p_get_source_type($ ) {
    my ( $path ) = @_;

    return &SOURCE_UNKNOWN unless ( -e $path );

    if ( -d File::Spec->catdir( $path, &DIR_SVN ) ) {
        return &SOURCE_SVN;
    }
    elsif ( -d File::Spec->catdir( $path, &DIR_GIT ) ) {
        return &SOURCE_GIT;
    }
    elsif ( -d File::Spec->catdir( $path, &DIR_HG ) ) {
        return &SOURCE_HG;
    }

    my $real_path = realpath( File::Spec->canonpath( $path ) );
    my ( $vol, $dir, $file ) = File::Spec->splitpath( $real_path, 1 );
    my @path_dirs = File::Spec->splitdir( $dir );
    my $check_svn = ( -f $path ? 1 : undef );

    while ( @path_dirs ) {
        pop @path_dirs;
        my $try_path = File::Spec->catpath( $vol, File::Spec->catdir( @path_dirs ), "" );
        if ( $check_svn ) {
            my $try_svn_dir = File::Spec->catdir( $try_path, &DIR_SVN );
            return &SOURCE_SVN
                if ( -d $try_svn_dir );
            undef $check_svn;
        } ## end if ( $check_svn )
        my $try_git_dir = File::Spec->catdir( $try_path, &DIR_GIT );
        return &SOURCE_GIT
            if ( -d $try_git_dir );
        my $try_hg_dir = File::Spec->catdir( $try_path, &DIR_HG );
        return &SOURCE_HG
            if ( -d $try_hg_dir );
    } ## end while ( @path_dirs )
    return &SOURCE_UNKNOWN;
} ## end sub p_get_source_type($ )

sub p_get_version_info($ ) {
    my ( $path ) = @_;

    my $source_type = p_get_source_type( $path );
    my $alt_path    = $path;
    if ( $^O ne "MSWin32" && -l $path ) {
        $alt_path = realpath( $path );
    }
    return (
          $source_type eq &SOURCE_SVN ? get_svn_info( $alt_path )
        : $source_type eq &SOURCE_GIT ? get_git_info( $alt_path )
        : $source_type eq &SOURCE_HG  ? get_hg_info( $alt_path )
        : generic_get_info( $alt_path )
    );
} ## end sub p_get_version_info($ )

## p_generate_base_name
# Returns raw_name,base_name,base_path
sub p_generate_base_name($$ ) {
    my ( $addon_or_path, $base_path ) = @_;
    return undef unless defined $addon_or_path;
    my @pieces = File::Spec->splitdir( $addon_or_path );
    return undef unless @pieces;
    my $addon      = pop @pieces;
    my $addon_info = AddonInfo->lookup( $addon );
    if ( @pieces ) {
        return $addon, $addon, File::Spec->catdir( @pieces );
    }
    else {

        # Not a path...
        my $name              = $addon;
        my $enabled_base_path = enabled_dir( $base_path );
        my $is_install
            = ( is_same_path( $enabled_base_path, default_install_dir ) || is_same_path( $enabled_base_path, install_dir ) );
        if ( $addon_info ) {
            if ( ( !$is_install ) && $addon_info->is_split ) {
                $name = $addon_info->get_base_addon;
            }
            else {
                $name = $addon_info->get_name;
            }
        } ## end if ( $addon_info )
        return $addon, $name, $base_path;
    } ## end else [ if ( @pieces ) ]
} ## end sub p_generate_base_name($$ )

sub p_compare_entries($$ ) {
    my ( $left, $right ) = @_;
    my ( $left_rev,  $left_minor,  $left_disabled,  $left_time )  = @$left;
    my ( $right_rev, $right_minor, $right_disabled, $right_time ) = @$right;
    return ( ( $right_disabled cmp $left_disabled ) || ( $left_time <=> $right_time ) || ( $left_rev <=> $right_rev ) || ( $left_minor <=> $right_minor ) );
} ## end sub p_compare_entries($$ )

sub p_move_path($$$ ) {
    my ( $current_base_path, $desired_base_path, $entry ) = @_;

    my $current_path = File::Spec->catfile( $current_base_path, $entry );
    my $desired_path = File::Spec->catfile( $desired_base_path, $entry );

    return undef unless -e $current_path;
    return undef if -e $desired_path;

    if ( p_get_source_type( $current_base_path ) ne &SOURCE_UNKNOWN ) {
        print STDERR "Unable to move " . $current_path . ": " . $current_base_path . " is under version control\n";
        return undef;
    }
    if ( p_get_source_type( $desired_base_path ) ne &SOURCE_UNKNOWN ) {
        print STDERR "Unable to move " . $current_path . " to " . $desired_path . ": " . $desired_base_path . " is under version control\n";
        return undef;
    }

    if ( $current_path eq $desired_path ) {
        return undef;
    }

    if ( -l $current_path ) {
        my $target_path = realpath( $current_path );
        my $target_rel = File::Spec->abs2rel( $target_path, $desired_base_path );
        print_command( "ln", "-s", $target_rel, $desired_path ) if ( $opt_output{ "verbose" } );
        symlink( $target_rel, $desired_path ) unless $opt_flags{ "dryrun" };
        print_command( "rm", $current_path ) if ( $opt_output{ "verbose" } );
        unlink( $current_path ) unless $opt_flags{ "dryrun" };
    } ## end if ( -l $current_path )
    else {
        print_command( "mv", $current_path, $desired_path ) if ( $opt_output{ "verbose" } );
        rename( $current_path, $desired_path ) unless $opt_flags{ "dryrun" };
    }

    return 1;
} ## end sub p_move_path($$$ )

our %path_to_key_cache = ();

sub p_get_archive($ ) {
    my $try_file = shift;
    return undef unless -f $try_file;

    my ( $vol, $dir, $entry ) = File::Spec->splitpath( $try_file );
    return undef unless defined $entry;
    return undef if $entry eq "";

    my $key = get_file_key( $try_file );
    return undef unless defined $key;
    $key .= "." . $try_file;

    my $zip       = $ziparchives_by_file_key{ $key };
    my $files     = $zipfiles_by_file_key_by_file_ucname{ $key };
    my $dirs      = $zipdirs_by_file_key_by_file_ucname{ $key };
    my $best_time = $ziptimes_by_file_key{ $key };
    unless ( $zip ) {
        my ( $name, $rev, $minor_rev, $extension ) = split_entry( $entry );

        if ( $extension eq "zip" ) {
            $zip = Archive::Zip->new( $try_file );
        }
        elsif ($extension eq "tar"
            || $extension eq "tar.gz"
            || $extension eq "tar.bz2"
            || $extension eq "tar.Z" )
        {
            my $is_gz       = $extension eq "tar.gz";
            my $is_bz2      = $extension eq "tar.bz2";
            my $is_compress = $extension eq "tar.Z";
            my $compress;
            my $use_temp;

            my $tar_fh;
            if ( $is_compress ) {
                $use_temp = 1;
            }
            elsif ( $is_gz ) {
                $compress = 1;

                #	open( $tar_fh, "<:gzip", $try_file )
                #	  or undef $tar_fh;
            } ## end elsif ( $is_gz )
            elsif ( $is_bz2 ) {
                open( $tar_fh, "<:via(Bzip2)", $try_file )
                    or undef $tar_fh;
            }

            $zip = Archive::Zip->new;
            my $tar;
            unless ( $use_temp ) {
                $tar = Archive::Tar->new( $tar_fh || $try_file, $compress )
                    or $use_temp = 1;
            }

            unless ( $use_temp ) {
                my $zip_member;
                foreach my $tar_member ( $tar->get_files ) {
                    undef $zip_member;
                    next if ( $tar_member->name =~ m{^(.*/)?\._} );
                    if (   $tar_member->is_file
                        || $tar_member->is_symlink )
                    {
                        my $contents = "" . $tar_member->get_content;
                        $zip_member = $zip->addString(
                            $contents,
                            $tar_member->name
                        );
                    } ## end if ( $tar_member->is_file...)
                    elsif ( $tar_member->is_dir ) {
                        $zip_member = $zip->addDirectory( "", $tar_member->name );
                    }
                    if ( defined $zip_member ) {
                        $zip_member->unixFileAttributes( $tar_member->mode );
                        $zip_member->setLastModFileDateTimeFromUnix( $tar_member->mtime );
                    }
                } ## end foreach my $tar_member ( $tar...)
            } ## end unless ( $use_temp )

            if ( $use_temp ) {
                my $base = $name;

                # Tar / Tar gz archive...
                # Need to unpack it and find all the addons within...
                my $tar_cmd = find_command( "tar" );
                return undef unless $tar_cmd;
                my $dir = get_temp_path( $base );
                my @cmd = ( $tar_cmd, "-x" );
                push @cmd, "-v" if $opt_output{ "verbose" };
                push @cmd, "-Z" if $is_compress;
                push @cmd, "-z" if $is_gz;
                push @cmd, "-j" if $is_bz2;
                push @cmd, "-f", $try_file;
                push @cmd, "-C", $dir;
                print_command( @cmd ) if $opt_output{ "verbose" };
                do_command( @cmd );
                $zip->addTree( $dir, "" );
            } ## end if ( $use_temp )

        } ## end elsif ( $extension eq "tar"...)
        else {
            return undef;
        }
        $ziparchives_by_file_key{ $key }             = $zip;
        $files                                       = {};
        $dirs                                        = {};
        $zipfiles_by_file_key_by_file_ucname{ $key } = $files;
        $zipdirs_by_file_key_by_file_ucname{ $key }  = $dirs;
        undef $best_time;

        # Merge in additional files
        {
            my @dirs    = File::Spec->splitdir( $dir );
            my $lastdir = pop @dirs;
            if ( defined $lastdir && $lastdir eq "" ) {
                $lastdir = pop @dirs;
            }
            if ( lc $lastdir eq lc &DIR_DOWNLOADS ) {
                my $merge_root = File::Spec->catpath( $vol, File::Spec->catdir( @dirs ), &DIR_MERGE );

                my @match_keys;
                my @members = $zip->members;
                foreach my $member ( @members ) {
                    my $name = $member->fileName();
                    next unless $name =~ m{\.toc$}i;
                    my @toc_dir       = File::Spec::Unix->splitdir( $name );
                    my $toc_name      = pop @toc_dir;
                    my $toc_path      = File::Spec::Unix->catdir( @toc_dir );
                    my $subaddon_name = $toc_name;
                    $subaddon_name =~ s{\.toc$}{}i;
                    push @match_keys, ( uc $toc_path );
                    my $merge_path = File::Spec->catdir( $merge_root, $subaddon_name );

                    if ( -d $merge_path ) {
                        $zip->addTree( $merge_path, $toc_path );
                    }
                } ## end foreach my $member ( @members)

                my %merge_archives = ();
                my $dirh           = IO::Dir->new( $merge_root );
                if ( $dirh ) {
                    my ( $best_zip, $best_files, $best_dirs, $best_time, $best_entry );
                    foreach my $entry ( $dirh->read ) {
                        my $try_file = File::Spec->catfile( $merge_root, $entry );
                        if ( -f $try_file ) {
                            my ( $try_zip, $files, $dirs, $time ) = p_get_archive( $try_file );
                            my $consider;
                        TRY_MATCHES:
                            foreach my $match_key ( @match_keys ) {
                                if ( exists $dirs->{ $match_key } ) {
                                    $consider = 1;
                                    last TRY_MATCHES;
                                }
                            } ## end foreach my $match_key ( @match_keys)
                            if ( $consider ) {
                                if ( !defined $best_time || $time > $best_time ) {
                                    ( $best_zip, $best_files, $best_dirs, $best_time, $best_entry ) = ( $try_zip, $files, $dirs, $time, $entry );
                                }
                            }
                        } ## end if ( -f $try_file )
                    } ## end foreach my $entry ( $dirh->read)
                    $dirh->close;
                    undef $dirh;
                    if ( $best_dirs && $best_files ) {
                        my @to_merge = @match_keys;
                        while ( @to_merge ) {
                            my $merge_dir = shift @to_merge;
                            my $dir       = $best_dirs->{ $merge_dir };
                            if ( $dir ) {
                                while ( my ( $fname, $fpath ) = each %$dir ) {
                                    if ( exists $best_files->{ $fpath } ) {
                                        $zip->addMember( $best_files->{ $fpath } );
                                    }
                                    elsif ( exists $best_dirs->{ $fpath } ) {
                                        push @to_merge, $fpath;
                                    }
                                } ## end while ( my ( $fname, $fpath...))
                            } ## end if ( $dir )
                        } ## end while ( @to_merge )
                    } ## end if ( $best_dirs && $best_files)
                } ## end if ( $dirh )
            } ## end if ( lc $lastdir eq lc...)
        }

        foreach my $member ( $zip->members ) {
            my $name = $member->fileName();
            my $time = $member->lastModTime;
            $best_time = $time unless defined $best_time;
            $best_time = $time if $time > $best_time;
            my @name_pieces = File::Spec::Unix->splitdir( $name );
            if ( $member->isDirectory ) {
                if ( @name_pieces ) {
                    my $last = $name_pieces[ -1 ];
                    push @name_pieces, "" unless $last eq "";
                }
                else {
                    push @name_pieces, "";
                }
            } ## end if ( $member->isDirectory)
            else {
                my $fname = File::Spec->catfile( @name_pieces );
                $files->{ uc $fname } = $member;
            }
            while ( @name_pieces ) {
                my $last = pop @name_pieces;
                my $dir  = File::Spec->catdir( @name_pieces );
                my $rec  = $dirs->{ uc $dir } ||= {};
                next if $last eq "";
                my $fname = File::Spec->catfile( @name_pieces, $last );
                $rec->{ $last } = uc $fname;
            } ## end while ( @name_pieces )
        } ## end foreach my $member ( $zip->members)
        unless ( defined $best_time ) {
            $best_time = ( stat( $try_file ) )[ 9 ];
        }
        $ziptimes_by_file_key{ $key } = $best_time;
        Hash::Util::lock_hashref( $files );
        Hash::Util::lock_hashref( $dirs );
    } ## end unless ( $zip )
    return $zip, $files, $dirs, $best_time if wantarray;
    return $zip;
} ## end sub p_get_archive($ )

sub p_get_subaddons($ ) {
    my $try_path = shift;
    my @res      = ();

    if ( -f $try_path ) {
        my ( $zip, $files, $dirs, $best_times ) = p_get_archive( $try_path );
        return @res unless $zip;

        my @dirs_to_try = ( "" );
        while ( @dirs_to_try ) {
            my $subdir = shift @dirs_to_try;
            my $dir    = $dirs->{ uc $subdir };
            next unless defined $dir;
            my @new_dirs = ();
            my $any_source;
            foreach my $entry ( keys %$dir ) {
                my $fname = $dir->{ $entry };
                if ( $entry =~ m{\.toc$}i ) {
                    @new_dirs = ();
                    my $subaddon = ( File::Spec::Unix->splitpath( $files->{ $fname }->fileName ) )[ 2 ];
                    $subaddon =~ s{\.toc$}{}i;
                    push @res, [ $subaddon, $subdir ];
                    undef $any_source;
                    last;
                } ## end if ( $entry =~ m{\.toc$}i)
                elsif ( exists $dirs->{ $fname } ) {
                    push @new_dirs,
                        (
                          $subdir eq ""
                        ? $entry
                        : File::Spec->catdir( $subdir, $entry )
                        );
                } ## end elsif ( exists $dirs->{ $fname...})
                elsif ( $subdir eq "" && $entry =~ m{\.(?:lua|xml)$}i ) {
                    $any_source = 1;
                }
            } ## end foreach my $entry ( keys %$dir)
            if ( $any_source ) {
                my $subaddon = ( File::Spec::Unix->splitpath( $try_path ) )[ 2 ];
                if ( $subaddon =~ m{^lib}i ) {
                    $subaddon =~ s{\.(?:t(?:gz|ar(?:\.gz)?)|zip|gz)$}{};
                    @new_dirs = ();
                    push @res, [ $subaddon, $subdir ];
                }
            } ## end if ( $any_source )
            push @dirs_to_try, @new_dirs;
        } ## end while ( @dirs_to_try )
    } ## end if ( -f $try_path )
    else {
        my @dirs_to_try = ( "" );
        while ( @dirs_to_try ) {
            my $subdir  = shift @dirs_to_try;
            my $dirname = File::Spec->catdir( $try_path, $subdir );
            my $dirh    = new IO::Dir( $dirname );
            my $any_source;
            next unless $dirh;
            my @new_dirs = ();
            foreach my $entry ( File::Spec->no_upwards( $dirh->read ) ) {
                if ( $entry =~ m{\.toc$}i ) {
                    @new_dirs = ();
                    my $subaddon = $entry;
                    $subaddon =~ s{\.toc$}{}i;
                    push @res, [ $subaddon, $subdir ];
                    undef $any_source;
                    last;
                } ## end if ( $entry =~ m{\.toc$}i)
                elsif ( -d File::Spec->catdir( $dirname, $entry ) ) {
                    push @new_dirs,
                        (
                          $subdir eq ""
                        ? $entry
                        : File::Spec->catdir( $subdir, $entry )
                        );
                } ## end elsif ( -d File::Spec->catdir...)
                elsif ( $subdir eq "" && $entry =~ m{\.(?:lua|xml)$}i ) {
                    $any_source = 1;
                }
            } ## end foreach my $entry ( File::Spec...)
            if ( $any_source ) {
                my $subaddon = ( File::Spec::Unix->splitpath( $try_path ) )[ 2 ];
                if ( $subaddon =~ m{^lib}i ) {
                    @new_dirs = ();
                    push @res, [ $subaddon, $subdir ];
                }
            } ## end if ( $any_source )
            push @dirs_to_try, @new_dirs;
        } ## end while ( @dirs_to_try )
    } ## end else [ if ( -f $try_path ) ]
    return @res;
} ## end sub p_get_subaddons($ )

our %valid_version_seperator = (
    ""  => 1,
    "_" => 1,
    "." => 1,
    " " => 1,
    "-" => 1,
    "v" => 1
);

sub p_get_all_entries($ ) {
    my $self = shift;

    return unless $self->[ &field_group_okay ];

    my $enabled_base_path  = $self->[ &field_enabled_base_path ];
    my $disabled_base_path = $self->[ &field_disabled_base_path ];
    my $name               = $self->[ &field_base_name ];
    my $ucname             = uc $name;

    my $toc_name_top = $ucname . ".TOC";
    my $toc_name_sdir = File::Spec::Unix->catfile( $ucname, $toc_name_top );

    my %rec = ();
    foreach my $group ( "", &DIR_DOWNLOADS, AddonInfo->get_groups ) {
        next unless defined $group;
        next if $rec{ $group };
        my $group_rec = {};
        $rec{ $group } = $group_rec;

        foreach my $base_path ( $disabled_base_path, $enabled_base_path ) {
            my $group_path = $base_path;
            $group_path = File::Spec->catdir( $group_path, $group ) if $group ne "";

            my $dirh = new IO::Dir( $group_path );
            if ( $dirh ) {
                foreach my $entry ( $dirh->read ) {

                    #my $ucprefix = uc substr($entry,0,length($ucname));
                    #next unless $ucprefix eq $ucname;
                    #my $sep = substr($entry,length($ucname),1);
                    #next unless $valid_version_seperator{$sep};

                    my ( $entry_name, $rev, $minor_rev, $extension ) = split_entry( $entry );

                    my $try_file = File::Spec->catfile( $group_path, $entry );
                    my $best_time;
                    if ( $group eq &DIR_DOWNLOADS && -f $try_file ) {
                        my ( $zip, $files, $dirs );
                        ( $zip, $files, $dirs, $best_time ) = p_get_archive( $try_file );
                        if ( $files ) {
                            next unless ( exists $files->{ $toc_name_top } || exists $files->{ $toc_name_sdir } );
                        }
                        else {
                            next unless $ucname eq uc $entry_name;
                        }
                    } ## end if ( $group eq &DIR_DOWNLOADS...)
                    else {
                        next unless $ucname eq uc $entry_name;
                    }

                    unless ( $rev ) {
                        my $try_dir = File::Spec->catdir( $group_path, $entry );
                        if ( -d $try_dir ) {
                            my $version_info = p_get_version_info( $try_dir );
                            $rev = $version_info && $version_info->{ &VINFO_DIR_REVISION } || 1;
                            $rev = 1 unless ( $rev =~ m{^\d+$} );
                        }
                        else {
                            $rev = 0;
                        }
                    } ## end unless ( $rev )
                    $minor_rev = -1 unless defined $minor_rev;
                    $best_time = -1 unless defined $best_time;
                    $group_rec->{ $entry } = [ $rev, $minor_rev, ( ( $base_path eq $disabled_base_path ) ? 1 : 0 ), $best_time ];
                } ## end foreach my $entry ( $dirh->read)
                $dirh->close;
                undef $dirh;
            } ## end if ( $dirh )
        } ## end foreach my $base_path ( $disabled_base_path...)
    } ## end foreach my $group ( "", &DIR_DOWNLOADS...)
    return %rec;
} ## end sub p_get_all_entries($ )

sub p_get_base_path($ ) {
    my $self = shift;
    return (
          $self->[ &field_is_disabled_dir ]
        ? $self->[ &field_disabled_base_path ]
        : $self->[ &field_enabled_base_path ]
    );
} ## end sub p_get_base_path($ )

sub p_get_file_in_zip($$ ) {
    my ( $self, $fname ) = @_;

    my ( $zip, $files, $dirs ) = $self->p_get_zip;
    if ( $files ) {
        return $files->{ uc $fname };
    }
} ## end sub p_get_file_in_zip($$ )

sub p_get_dir_in_zip($$ ) {
    my ( $self, $dname ) = @_;

    my ( $zip, $files, $dirs ) = $self->p_get_zip;
    if ( $dirs ) {
        return $dirs->{ uc $dname };
    }
} ## end sub p_get_dir_in_zip($$ )

sub p_node_ls($$ ) {
    my ( $self, $rel ) = @_;
    my $dirs  = {};
    my $files = {};

    my $try_file = $self->get_real_path;
    if ( -d $try_file ) {

        # Normal directory
        my $path = File::Spec->catdir( $try_file, $rel );
        my $dirh = new IO::Dir( $path );
        if ( $dirh ) {
            foreach my $name ( File::Spec->no_upwards( $dirh->read ) ) {
                my $frel = (
                      $rel eq ""
                    ? $name
                    : File::Spec->catfile( $rel, $name )
                );
                my $fnode = $self->get_node_path( $frel );
                if ( -d File::Spec->catdir( $path, $name ) ) {
                    $dirs->{ $name } = $fnode;
                }
                else {
                    $files->{ $name } = $fnode;
                }
            } ## end foreach my $name ( File::Spec...)
            $dirh->close;
            undef $dirh;
        } ## end if ( $dirh )
    } ## end if ( -d $try_file )
    else {
        my ( $zip, $zip_files, $zip_dirs ) = $self->p_get_zip;
        my $dir = $zip_dirs && $zip_dirs->{ uc $rel };
        if ( $dir ) {
            while ( my ( $fname, $fpath ) = each %$dir ) {
                my $frel = (
                      $rel eq ""
                    ? $fname
                    : File::Spec->catfile( $rel, $fname )
                );
                my $fnode = $self->get_node_path( $frel );
                if ( exists $zip_files->{ $fpath } ) {
                    $files->{ $fname } = $fnode;
                }
                else {
                    $dirs->{ $fname } = $fnode;
                }
            } ## end while ( my ( $fname, $fpath...))
        } ## end if ( $dir )
    } ## end else [ if ( -d $try_file ) ]
    return $dirs, $files;
} ## end sub p_node_ls($$ )

sub p_node_is_file($$ ) {
    my ( $self, $rel ) = @_;

    my $try_file = $self->get_real_path;
    return undef if $rel eq "";
    if ( -d $try_file ) {
        return ( -f File::Spec->catfile( $try_file, $rel ) ? 1 : undef );
    }
    else {
        my ( $zip, $zip_files, $zip_dirs ) = $self->p_get_zip;
        my $file = $zip_files && $zip_files->{ uc $rel };
        return ( defined $file ? 1 : undef );
    }
} ## end sub p_node_is_file($$ )

sub p_node_version_info($$ ) {
    my ( $self, $rel ) = @_;

    my $try_file = $self->get_real_path;
    return undef unless ( -d $try_file );
    my $path = File::Spec->catfile( $try_file, $rel );
    return p_get_version_info( $path );
} ## end sub p_node_version_info($$ )

sub p_node_svn_externals($$ ) {
    my ( $self, $rel ) = @_;

    my $try_file = $self->get_real_path;
    return undef unless ( -d $try_file );
    my $path = File::Spec->catdir( $try_file, $rel );
    my $alt_path = $path;
    if ( $^O ne "MSWin32" && -l $path ) {
        $alt_path = realpath( $path );
    }
    return get_svn_externals( $alt_path );
} ## end sub p_node_svn_externals($$ )

sub p_node_file_key_raw($$ ) {
    my ( $self, $rel ) = @_;

    my $try_file = $self->get_real_path;
    if ( -d $try_file ) {

        # Normal directory
        my $path = File::Spec->catfile( $try_file, $rel );
        return get_file_key( $path );
    } ## end if ( -d $try_file )
    else {
        my $key = get_file_key( $try_file );
        return undef unless defined $key;
        my ( $zip, $zip_files, $zip_dirs ) = $self->p_get_zip;
        my $file = $zip_files && exists $zip_files->{ uc $rel };
        my $dir  = $zip_dirs  && exists $zip_dirs->{ uc $rel };
        if ( ( defined $file ) || ( defined $dir ) ) {
            return join( "/", $key, $rel );
        }
        return undef;
    } ## end else [ if ( -d $try_file ) ]
} ## end sub p_node_file_key_raw($$ )

my %replacements = ();

sub p_node_file_key($$ ) {
    my ( $self, $rel ) = @_;

    my $key = $self->p_node_file_key_raw( $rel );
    $key .= '/replaced' if ( defined $key
        && exists $replacements{ $key } );
    return $key;
} ## end sub p_node_file_key($$ )

sub p_node_replace_contents($$$ ) {
    my $self = shift;
    my $rel  = shift;
    my $key  = $self->p_node_file_key_raw( $rel );
    return undef unless defined $key;
    $replacements{ $key } = $_[ 0 ];
    return $key;
} ## end sub p_node_replace_contents($$$ )

sub p_node_contents($$ ) {
    my ( $self, $rel ) = @_;
    my $contents;
    my $mtime;

    my $key = $self->p_node_file_key_raw( $rel );
    $contents = $replacements{ $key } if defined $key;

    my $try_file = $self->get_real_path;
    if ( -d $try_file ) {

        # Normal directory
        my $path = File::Spec->catfile( $try_file, $rel );
        if ( -f $path ) {
            my $mtime = ( stat( _ ) )[ 9 ];
            unless ( defined $contents ) {
                $contents = read_file( $path, binmode => ':raw' );
            }
        } ## end if ( -f $path )
    } ## end if ( -d $try_file )
    else {
        my ( $zip, $zip_files, $zip_dirs ) = $self->p_get_zip;
        my $file = $zip_files && exists $zip_files->{ uc $rel } && $zip_files->{ uc $rel };
        if ( $file ) {
            unless ( defined $contents ) {
                $contents = $file->contents;
            }
            $mtime = $file->lastModTime;
        } ## end if ( $file )
    } ## end else [ if ( -d $try_file ) ]
    return $contents, $mtime if wantarray;
    return $contents;
} ## end sub p_node_contents($$ )

sub p_node_copy_to_path($$$ ) {
    my ( $self, $rel, $dest ) = @_;

    my $key = $self->p_node_file_key_raw( $rel );
    if ( defined $key && exists $replacements{ $key } ) {
        write_file( $dest, { binmode => ':raw' }, $replacements{ $key } );
        return 1;
    }
    my $try_file = $self->get_real_path;
    if ( -d $try_file ) {

        # Normal directory
        my $path = File::Spec->catfile( $try_file, $rel );
        return copy( $path, $dest );
    } ## end if ( -d $try_file )
    else {
        my ( $zip, $zip_files, $zip_dirs ) = $self->p_get_zip;
        my $file = $zip_files && $zip_files->{ uc $rel };
        if ( $file ) {
            my $status = $file->extractToFileNamed( $dest );
            return undef unless $status == &Archive::Zip::AZ_OK;
        }
        else {
            $! = &Errno::ENOENT;
            return undef;
        }
    } ## end else [ if ( -d $try_file ) ]
} ## end sub p_node_copy_to_path($$$ )

sub p_node_copy_to_node($$$$ ) {
    my ( $self, $self_rel, $dest, $dest_rel ) = @_;

    my $dest_try_file = $dest->get_real_path;
    if ( -d $dest_try_file ) {

        # Normal directory
        my $dest = File::Spec->catfile( $dest_try_file, $dest_rel );
        return $self->p_node_copy_to_path( $self_rel, $dest );
    } ## end if ( -d $dest_try_file)
    else {
        $! = &Errno::EPERM;
        return undef;
    }
} ## end sub p_node_copy_to_node($$$$ )

sub p_node_diff_from_path($$$;$ ) {
    my ( $self, $self_rel, $dest, $check_only ) = @_;
    my @res      = ();
    my $try_file = $self->get_real_path;
    ### Src name might be a "virtual" path
    my $self_name = File::Spec->catfile( $try_file, $self_rel );
    my $self_contents;

    my $key = $self->p_node_file_key_raw( $self_rel );
    if ( defined $key && exists $replacements{ $key } ) {
        $self_contents = $replacements{ $key };
    }
    elsif ( -d $try_file ) {

        # Normal directory -- file to file diff
        if ( !-e $dest ) {
            if ( -e $self_name ) {
                push @res, diff_only_in( $self_name );
                return @res;
            }
            return @res;    ### Neither exists
        } ## end if ( !-e $dest )
        elsif ( !-e $self_name ) {
            push @res, diff_only_in( $dest );
            return @res;
        }
        elsif ( compare( $dest, $self_name ) == 0 ) {
            return @res;
        }
        elsif ( $check_only ) {
            push @res, "Files " . $dest . " and " . $self_name . " differ\n";
            return @res;
        }
        $self_contents = read_file( $self_name, binmode => ':raw' );
        unless ( defined $self_contents ) {
            push @res, "Files " . $dest . " and " . $self_name . " differ\n";
        }
    } ## end elsif ( -d $try_file )
    else {
        my ( $zip, $zip_files, $zip_dirs ) = $self->p_get_zip;
        my $file = $zip_files && $zip_files->{ uc $self_rel };
        if ( !-e $dest ) {
            if ( $file ) {
                ## Virtual path here...
                push @res, diff_only_in( $self_name );
                return @res;
            }
            return @res;    ### Neither exists
        } ## end if ( !-e $dest )
        elsif ( !defined $file ) {
            push @res, diff_only_in( $dest );
            return @res;
        }
        else {
            $self_contents = $file->contents;
            unless ( defined $self_contents ) {
                push @res, "Files " . $dest . " and " . $self_name . " differ\n";
                return @res;
            }
        } ## end else [ if ( !-e $dest ) ]
    } ## end else [ if ( defined $key && exists...)]

    my $contents = read_file( $dest, binmode => ':raw' );
    unless ( defined $contents ) {
        push @res, "Files " . $dest . " and " . $self_name . " differ\n";
        return @res;
    }

    if ( $contents ne $self_contents ) {
        if ( $check_only ) {
            push @res, "Files " . $dest . " and " . $self_name . " differ\n";
            return @res;
        }
        else {
            return diff_contents( $contents, $dest, $self_contents, $self_name );
        }
    } ## end if ( $contents ne $self_contents)
    return @res;
} ## end sub p_node_diff_from_path($$$;$ )

sub p_node_diff_from_node($$$$;$ ) {
    my ( $self, $self_rel, $dest, $dest_rel, $check_only ) = @_;
    my @res = ();

    my $dest_try_file = $dest->get_real_path;
    my $dest_name = File::Spec->catfile( $dest_try_file, $dest_rel );
    if ( -d $dest_try_file ) {

        # Normal directory
        push @res, $self->p_node_diff_from_path( $self_rel, $dest_name, $check_only );
        return @res;
    } ## end if ( -d $dest_try_file)
    my $self_name     = File::Spec->catfile( $self->get_real_path, $self_rel );
    my $self_contents = $self->p_node_contents( $self_rel );
    my $contents      = $dest->p_node_contents( $dest_rel );

    if ( !defined $self_contents ) {
        if ( defined $contents ) {
            push @res, diff_only_in( $dest_name );
            return @res;
        }
        return @res;
    } ## end if ( !defined $self_contents)
    elsif ( !defined $contents ) {
        push @res, diff_only_in( $self_name );
        return @res;
    }

    if ( $contents ne $self_contents ) {
        if ( $check_only ) {
            push @res, "Files " . $dest . " and " . $self_name . " differ\n";
            return @res;
        }
        else {
            return diff_contents( $contents, $dest, $self_contents, $self_name );
        }
    } ## end if ( $contents ne $self_contents)
    return @res;
} ## end sub p_node_diff_from_node($$$$;$ )

sub p_set_disabled_dir($$ ) {
    my ( $self, $disabled ) = @_;
    if ( ( $disabled ? 1 : 0 ) == ( $self->[ &field_is_disabled_dir ] ? 1 : 0 ) ) {
        return $self;
    }

    my $group         = "";
    my $current_group = "";
    if ( $self->[ &field_group_okay ] ) {
        my $info = AddonInfo->lookup( $self->[ &field_base_name ] );
        $group         = $info && $info->get_group || "";
        $current_group = $self->[ &field_group_cache ];
        $group         = $current_group if ( uc $current_group eq uc &DIR_DOWNLOADS );
    } ## end if ( $self->[ &field_group_okay...])

    my @desired_group_path = (
          $disabled
        ? $self->[ &field_disabled_base_path ]
        : $self->[ &field_enabled_base_path ]
    );
    push @desired_group_path, $group if $group ne "";
    my $desired_group_path = File::Spec->catdir( @desired_group_path );

    my @current_group_path = ( $self->p_get_base_path );
    push @current_group_path, $current_group if $current_group ne "";
    my $current_group_path = File::Spec->catdir( @current_group_path );

    $self->[ &field_move_tried ] = 1;
    if ( p_move_path( $current_group_path, $desired_group_path, $self->[ &field_entry ] ) ) {
        unless ( $opt_flags{ "dryrun" } ) {
            $self->[ &field_is_disabled_dir ] = $disabled;
            $self->[ &field_group_cache ]     = $group;
        }
    } ## end if ( p_move_path( $current_group_path...))

    return $self;
} ## end sub p_set_disabled_dir($$ )

sub lookup($$;$ ) {
    my ( $this, $path, $base_path ) = @_;
    my $class = ref( $this ) || $this;
    my ( $raw_name, $base_name, $info );
    unless ( defined $base_path ) {
        $base_path = $this->p_get_base_path if ref( $this );
    }
    $base_path = stage_dir unless ( defined $base_path );
    ( $raw_name, $base_name, $base_path ) = p_generate_base_name( $path, $base_path );
    return undef unless defined $base_path;
    my $enabled_base_path  = enabled_dir( $base_path );
    my $disabled_base_path = disabled_dir( $base_path );
    my $record             = $paths_by_enabled_path{ $enabled_base_path } ||= {};
    my $self               = $record->{ $base_name };

    unless ( $self ) {
        ### Determine best file
        my $group_okay
            = ( is_different_path( $enabled_base_path, default_install_dir ) && is_same_path( $enabled_base_path, stage_dir ) );
        my $is_install
            = ( is_same_path( $enabled_base_path, default_install_dir ) || is_same_path( $enabled_base_path, install_dir ) );
        my $is_disabled = is_different_path( $base_path, $enabled_base_path );
        $self = [
            $base_name    # base_name
            , $enabled_base_path     # enabled_base_path
            , $disabled_base_path    # disabled_base_path
            , $base_name             # entry
            , {}                     # nodes
            , $group_okay            # group_okay
            , ""                     # group_cache
            , $is_install            # is_install
            , undef                  # is_disabled
            , undef
        ];                           # move_tried
        bless $self, $class;
        $record->{ $base_name } = $self;

        ### Determine entry and if disabled_base_path is needed
        if ( $group_okay ) {
            my ( $best_entry, $best_entry_info, $best_group );
            my $info = AddonInfo->lookup( $self->[ &field_base_name ] );
            my $group = ( $info && $info->get_group ) || "";
            $self->[ &field_group_cache ] = $group;

            my %entries = $self->p_get_all_entries;

            # Check all the groups
            while ( my ( $try_group, $group_rec ) = each %entries ) {
                while ( my ( $try_entry, $try_entry_info ) = each %$group_rec ) {
                    if ( defined $best_entry_info ) {
                        my $compare
                            = ( p_compare_entries( $best_entry_info, $try_entry_info ) || ( $try_group eq $group ? -1 : 0 ) || ( $try_group eq "" && $best_group ne $group ? -1 : 0 ) );
                        next if ( $compare >= 0 );
                    }
                    $best_entry      = $try_entry;
                    $best_entry_info = $try_entry_info;
                    $best_group      = $try_group;
                } ## end while ( my ( $try_entry, ...))
            } ## end while ( my ( $try_group, ...))
            if ( defined $best_entry ) {
                $self->[ &field_entry ]           = $best_entry;
                $self->[ &field_group_cache ]     = $best_group;
                $self->[ &field_is_disabled_dir ] = $best_entry_info->[ 2 ];
                if (  !defined $info
                    && uc $best_group eq uc &DIR_DOWNLOADS )
                {
                    AddonInfo->make_download( $self->[ &field_base_name ] );
                }
            } ## end if ( defined $best_entry)
        } ## end if ( $group_okay )
        else {
            if ( -d File::Spec->catfile( $enabled_base_path, $base_name ) ) {
                $self->[ &field_is_disabled_dir ] = 0;
            }
            elsif ( -d File::Spec->catfile( $disabled_base_path, $base_name ) ) {
                $self->[ &field_is_disabled_dir ] = 1;
            }
        } ## end else [ if ( $group_okay ) ]
    } ## end unless ( $self )

    return ( $self, $raw_name ) if wantarray;
    return $self;
} ## end sub lookup($$;$ )

sub same_base_for($@ ) {
    my $self = shift;
    if ( wantarray ) {
        my @res = ();
        foreach my $name ( @_ ) {
            my $path = $self->lookup( $name );
            push @res, $path;
        }
        return @res;
    } ## end if ( wantarray )
    else {
        my $path = $self->lookup( shift );
        return $path;
    }
} ## end sub same_base_for($@ )

sub get_base_key($ ) {
    my $self = shift;
    return File::Spec->catfile( $self->[ &field_enabled_base_path ], $self->[ &field_base_name ] );
}

sub change_entry($$ ) {
    my $self = shift;
    $self->[ &field_entry ] = shift;
    return $self;
}

sub get_real_path($ ) {
    my $self = shift;

    my $base_path = $self->p_get_base_path;

    if ( $self->[ &field_group_okay ] ) {
        my $info = AddonInfo->lookup( $self->[ &field_base_name ] );
        my $group = $info && $info->get_group || "";

        my @desired_group_path = ( $base_path );
        push @desired_group_path, $group if $group ne "";
        my $desired_group_path = File::Spec->catdir( @desired_group_path );
        my $desired_path       = File::Spec->catfile(
            $desired_group_path,
            $self->[ &field_entry ]
        );

        unless ( -d $desired_group_path ) {

            # Create directory if necessary
            if ( $opt_flags{ "dryrun" } ) {
                print "mkdir -p " . $desired_group_path . "\n" if $opt_output{ "verbose" };
            }
            else {
                mkpath( $desired_group_path, $opt_output{ "verbose" }, 0777 );
            }
        } ## end unless ( -d $desired_group_path)

        # Handle the case that we have to move the directory
        my $current_group = $self->[ &field_group_cache ];
        if ( $group ne $current_group ) {
            my @current_group_path = ( $base_path );
            push @current_group_path, $current_group if $current_group ne "";
            my $current_group_path = File::Spec->catdir( @current_group_path );
            my $current_path       = File::Spec->catfile(
                $current_group_path,
                $self->[ &field_entry ]
            );

            if ( uc $current_group eq uc &DIR_DOWNLOADS ) {
                return $current_path;
            }

            if ( $self->[ &field_move_tried ] ) {
                unless ( $opt_flags{ "dryrun" } ) {
                    $info && $info->set_group( $current_group );
                }
                return $current_path;
            } ## end if ( $self->[ &field_move_tried...])
            $self->[ &field_move_tried ] = 1;

            if ( p_move_path( $current_group_path, $desired_group_path, $self->[ &field_entry ] ) ) {
                $self->[ &field_group_cache ] = $group unless $opt_flags{ "dryrun" };
                return $desired_path;
            }
            else {
                $info && $info->set_group( $current_group );
                return $current_path;
            }
        } ## end if ( $group ne $current_group)

        return $desired_path;
    } ## end if ( $self->[ &field_group_okay...])
    else {
        return File::Spec->catfile( $base_path, $self->[ &field_entry ] );
    }
} ## end sub get_real_path($ )

sub prune($ ) {
    my $self = shift;

    return $self->[ &field_base_name ] unless $self->[ &field_group_okay ];
    $self->get_real_path;

    my $enabled_base_path  = $self->[ &field_enabled_base_path ];
    my $disabled_base_path = $self->[ &field_disabled_base_path ];
    my $entry              = $self->[ &field_entry ];
    my $group              = $self->[ &field_group_cache ];

    my %entries = $self->p_get_all_entries;
    while ( my ( $try_group, $group_rec ) = each %entries ) {
        next if ( uc $try_group eq &DIR_DOWNLOADS );
        while ( my ( $try_entry, $try_rev ) = each %$group_rec ) {
            next if ( $try_group eq $group && $try_entry eq $entry );
            my $path = (
                  $try_rev->[ 2 ]
                ? $disabled_base_path
                : $enabled_base_path
            );
            $path = File::Spec->catdir( $path, $try_group ) if $try_group ne "";
            $path = File::Spec->catfile( $path, $try_entry );
            rmtree( $path, $opt_output{ "verbose" } ) if ( !$opt_flags{ "dryrun" } && -e $path );
        } ## end while ( my ( $try_entry, ...))
    } ## end while ( my ( $try_group, ...))
} ## end sub prune($ )

sub get_node_path($$ ) {
    my ( $self, $rel_dir ) = @_;

    my $nodes = $self->[ &field_nodes ];
    return $nodes->{ $rel_dir } ||= NodePath->new( $self, $rel_dir );
} ## end sub get_node_path($$ )

sub get_subaddon_nodes($ ) {
    my $self = shift;
    my @res;

    foreach my $subaddons ( p_get_subaddons( $self->get_real_path ) ) {
        push @res, $self->get_node_path( $subaddons->[ 1 ] );
    }
    return @res;
} ## end sub get_subaddon_nodes($ )

1;
